你好,我是 LMOS。
前面我们学过了很多基础理论,你可能已经迫不及待,想把这些知识运用到应用层开发里了。所以从这应用篇开始,我们会学以致用,分析一些开发工作中的实际问题,挑战几个典型的综合应用场景。
这节课我会从浏览器开始讲起,浏览器是目前使用范围最广、使用人数最多的终端应用程序之一。作为互联网中最重要的端口,浏览器伴随着互联网的高速发展,发展也是日新月异。通过接下来的两节课,我希望带你看看巨型软件应用优秀的架构设计,同时也带你了解一下平时用到的浏览器里,有哪些技术原理比较关键。
浏览器原本是很简单的东西,只能渲染简单的页面,后来才逐步迈进百花齐放的阶段。
我先带你梳理一下浏览器的发展过程。了解了这段历史,你就会重新理解WebKit内核和Chrome浏览器的地位,知道它们是怎么演变而来的。
说起浏览器,我们就不得不提到1994年诞生的网景浏览器(从Mosaic浏览器衍生而来)。虽然网景浏览器只能展示最简单的 HTML 静态页面,不支持动态的脚本(JavaScript)和样式(CSS),但是它仍然获得了很大的成功。
在操作系统中,内核是最基本的功能,随着浏览器的发展,在浏览器中,现在同样也存在内核的概念。浏览器内核的作用相对更加简化,浏览器内核的英文名为 Rendering Engine,你可以把它理解成一个渲染引擎,用途就是把文件资源转化成可视化的图像结果。
浏览器常见的浏览器内核有:Blink、WebKit、Gecko、Trident 等,目前 WebKit 内核占据了非常大的的市场,包括 Chrome、Safari、安卓浏览器等市面上的主流浏览器,都使用了 WebKit 内核。
既然 WebKit 这么经典,我们就以它为例来看一下浏览器内核的架构。浏览器内核主要包含:HTML Parser,CSS Parser,Layout,JavaScript Engine 几部分,如下图所示:
我们简单看一下,上图中的几个关键部分承担了什么工作:
上面是一个简略的浏览器内核的功能,不过它仅仅是完成了核心的渲染过程,实际上浏览器则要复杂得多。
在 2013 年,Chromium 发布了 Blink 项目。这个项目是从 WebKit 项目独立出来的,它抽离出了一套新的编程接口和进程模型接口,同时浏览器内核屏蔽了 Chromium 底层的进程模型实现。
我们会以 Chromium 浏览器为例,来分析浏览器及其内核。后面是 Chromium 的简易架构图,为方便分析,我删去了部分细节。
在上图中,比较重要的是Content 模块以及 Content 接口。
你可以这样理解,Content 模块和接口是浏览器对渲染过程的抽象,它们将浏览器的渲染、插件、沙箱等功能,进行了包装和抽象,提供一个接口层,方便上层的应用调用。Chromium 中我们可以看到的浏览器可视化界面,它构建在 Content 接口之上,用于接收用户交互和展示界面,content shell 是一个简易版的浏览器,通常被第三方浏览器软件进行二次开发,它在 Andriod 系统上也应用广泛。
Chromium和 Blink最大的一个特性就是采用了新的进程模型和线程模型。在前面进程篇的课程中我们了解到,进程是应用程序运行时操作系统进行资源分配的最小容器,这些资源包括指令集、独立内存空间、IO、PCB 等等。
不过,进程虽然能帮助我们更方便地分配资源,也会引发一些问题:
线程则是CPU调度的基本单位。线程的优点显而易见,切换成本很低,只有少量 CPU 寄存器、堆栈等内容,线程的创建、销毁本身也有性能成本,但这个成本相对较低,而且通常可以通过线程池优化。
不过我们也要关注到不足之处,多线程应用编码复杂度高,这会带来后面这几个问题:
那么在目前常见的 Chrome 浏览器里,采用的是多进程还是单进程多线程模型呢?在我们的电脑中,我打开进程管理器,就可以看到浏览器的后台进程占用情况。通常后台都存在多个进程:
其实在一些旧的浏览器中,采用的是单进程多线程的模型,如 IE 浏览器;但是以 Chromium 浏览器为例的现代浏览器,采用的都是多进程架构。
那么为什么现代浏览器采用的是多进程架构呢?我们需要先分析一下,浏览器如果是单进程多线程会引发哪些问题。
首先是稳定性的问题。因为一个浏览器程序,是可以同时启动多个 Tab 的,浏览器多进程化的最大的好处就是,单个 Tab 的卡死、崩溃不会影响其它 Tab。
我们在浏览器程序内新建一个 Tab 时,就会启动一个新的渲染进程,Chrome 支持四种不同的进程模型模型:
Chromium默认采用 Process-per-site-instance 方式,不过我们可以在浏览器启动时传递一个命令行开关,用来指定浏览器的进程模型。
其次,是加载速度的问题。由于整个程序只存在一个进程,浏览器的 JS 代码和插件逻辑和页面渲染是运行在同一个进程中的,如果存在一些计算量很大的操作,这些计算量大的线程会抢占大量资源,从而导致其他的渲染逻辑无法正常执行。这会严重影响页面的加载速度,甚至造成崩溃。在多进程架构下,将插件提取为单独的进程,不会存在插件卡顿和崩溃影响整个浏览器的情况。
最后还有安全性的考虑。由于 JS 脚本和插件的存在,很容易利用浏览器的系统漏洞,进而获得整个计算机的权限,从而造成安全问题。而多进程架构很容易就可以实现沙箱控制。
虽然采用多进程模型就可以解决上述几个问题,但是多进程模型也不是银弹,它同样也会引发一些问题。
比如性能的问题,由于基础的指令无法共享,多进程会带来很大程度的资源浪费,这也是我们很多同学吐槽的“Chrome 非常吃内存”这个问题。由于每个 Tab 和插件都是一个独立的进程,所以在打开多个 Tab 或者插件的情况下,我们会看到系统的内存会疯狂飙升。
这其实是设计上存在的问题,随着时间的发展,硬件性能瓶颈的突破也会推动软件架构发展。现在计算机内存越来越大,没有过去那么宝贵了,所以不同的选择,都是有利有弊。
现在,我们知道了浏览器采用的是多进程模型,那么具体有哪些常见进程呢?我们结合后面这张图来看看。
图中的方框表示具体的进程,连线表示进程间进行的通信,没有连线则表示不会发生进程通信。比如多个渲染进程之间不会进行通信,它们会通过主进程交互, NPAPI插件也不会和 GPU 进行交互,因为它太过于古老,所以没有 GPU 的接口实现。
图中我们可以看到以下几种进程:
这里我们着重了解一下渲染进程(renderer)内的主要线程。渲染进程内部是多线程的,在内核控制下各线程相互配合以保持同步。那么渲染进程内部具体有哪些线程呢?一共有八类:
这节课告一段落,我给你做个总结吧。
今天我们先简单分析了WebKit和Chromium的架构,它们俩在浏览器里是非常经典的设计。
然后,我们讨论了 Chrome 浏览器里,采用的模型是多进程,还是单进程多线程。Chrome 浏览器使用了多进程模型解决了一些历史问题,它内部有负责统领全局的浏览器主进程、负责网络交互的网络进程、负责页面渲染的渲染进程、负责插件运行的独立插件进程以及负责3D效果的 GPU 进程。
这里重点说说渲染进程,它主要负责单个 Tab 内的页面渲染逻辑,在渲染进程内又是多线程的,在浏览器内核控制下各线程相互配合以保持同步,高效协作。
在各个进程和线程的配合下,浏览器会进行一系列的动作完成渲染,我们称之为渲染流水线,就是从接受网络请求开始,到将其处理成可展示的图形和用户进行交互的过程。那这个过程具体如何运转呢?我们下节课再继续讨论。
这节课导图如下所示,供你复习回顾:
浏览器的多进程模型下,进程之间是如何通信的呢?
期待你在留言区说说自己的思考。也推荐你把这节课分享给更多同事、朋友。
© 2019 - 2023 Liangliang Lee. Powered by gin and hexo-theme-book.